/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Reflection;
using System.Text;

using Microsoft.Scripting.Generation;
using Microsoft.Scripting.Runtime;
using Microsoft.Scripting.Utils;

namespace Microsoft.Scripting.Actions {
    /// <summary>
    /// Provides binding semantics for a language.  This include conversions as well as support
    /// for producing rules for actions.  These optimized rules are used for calling methods, 
    /// performing operators, and getting members using the ActionBinder's conversion semantics.
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling")]
    public abstract class DefaultBinder : ActionBinder {
        protected DefaultBinder(ScriptDomainManager manager) : base(manager) {
        }

        /// <summary>
        /// Produces a rule for the specified Action for the given arguments.
        /// 
        /// The default implementation can produce rules for standard .NET types.  Languages should
        /// override this and provide any custom behavior they need and fallback to the default
        /// implementation if no custom behavior is required.
        /// </summary>
        /// <typeparam name="T">The type of the DynamicSite the rule is being produced for.</typeparam>
        /// <param name="action">The Action that is being performed.</param>
        /// <param name="args">The arguments to the action as provided from the call site at runtime.</param>
        /// <returns></returns>
        protected override RuleBuilder<T> MakeRule<T>(DynamicAction/*!*/ action, object[]/*!*/ args) {
            ContractUtils.RequiresNotNull(action, "action");
            ContractUtils.RequiresNotNull(args, "args");

            object[] extracted;
            CodeContext callerContext = ExtractCodeContext(args, out extracted);

            ContractUtils.RequiresNotNull(callerContext, "callerContext");

            switch (action.Kind) {
                case DynamicActionKind.Call:
                    return new CallBinderHelper<T, CallAction>(callerContext, (CallAction)action, extracted).MakeRule();
                case DynamicActionKind.GetMember:
                    return new GetMemberBinderHelper<T>(callerContext, (GetMemberAction)action, extracted).MakeNewRule();
                case DynamicActionKind.SetMember:
                    return new SetMemberBinderHelper<T>(callerContext, (SetMemberAction)action, extracted).MakeNewRule();
                case DynamicActionKind.CreateInstance:
                    return new CreateInstanceBinderHelper<T>(callerContext, (CreateInstanceAction)action, extracted).MakeRule();
                case DynamicActionKind.DoOperation:
                    return new DoOperationBinderHelper<T>(callerContext, (DoOperationAction)action, extracted).MakeRule();
                case DynamicActionKind.DeleteMember:
                    return new DeleteMemberBinderHelper<T>(callerContext, (DeleteMemberAction)action, extracted).MakeRule();
                case DynamicActionKind.InvokeMember:
                    return new InvokeMemberBinderHelper<T>(callerContext, (InvokeMemberAction)action, extracted).MakeRule();
                case DynamicActionKind.ConvertTo:
                    return new ConvertToBinderHelper<T>(callerContext, (ConvertToAction)action, extracted).MakeRule();
                default:
                    throw new NotImplementedException(action.ToString());
            }
        }

        protected static CodeContext ExtractCodeContext(object[] args, out object[] extracted) {
            CodeContext cc;
            if (args.Length > 0 && (cc = args[0] as CodeContext) != null) {
                extracted = ArrayUtils.ShiftLeft(args, 1);
            } else {
                cc = null;
                extracted = args;
            }
            return cc;
        }

        public virtual ErrorInfo MakeInvalidParametersError(BindingTarget target) {
            switch (target.Result) {
                case BindingResult.CallFailure: return MakeCallFailureError(target);
                case BindingResult.AmbigiousMatch: return MakeAmbigiousCallError(target);
                case BindingResult.IncorrectArgumentCount: return MakeIncorrectArgumentCountError(target);
                default: throw new InvalidOperationException();
            }
        }

        private static ErrorInfo MakeIncorrectArgumentCountError(BindingTarget target) {
            int minArgs = Int32.MaxValue;
            int maxArgs = Int32.MinValue;
            foreach (int argCnt in target.ExpectedArgumentCount) {
                minArgs = System.Math.Min(minArgs, argCnt);
                maxArgs = System.Math.Max(maxArgs, argCnt);
            }

            return ErrorInfo.FromException(
                Ast.Expression.Call(
                    typeof(BinderOps).GetMethod("TypeErrorForIncorrectArgumentCount", new Type[] {
                                typeof(string), typeof(int), typeof(int) , typeof(int), typeof(int), typeof(bool), typeof(bool)
                            }),
                    Ast.Expression.Constant(target.Name, typeof(string)),  // name
                    Ast.Expression.Constant(minArgs),                      // min formal normal arg cnt
                    Ast.Expression.Constant(maxArgs),                      // max formal normal arg cnt
                    Ast.Expression.Constant(0),                            // default cnt
                    Ast.Expression.Constant(target.ActualArgumentCount),   // args provided
                    Ast.Expression.Constant(false),                        // hasArgList
                    Ast.Expression.Constant(false)                         // kwargs provided
                )
            );
        }

        private ErrorInfo MakeAmbigiousCallError(BindingTarget target) {
            StringBuilder sb = new StringBuilder("Multiple targets could match: ");
            string outerComma = "";
            foreach (MethodTarget mt in target.AmbigiousMatches) {
                Type[] types = mt.GetParameterTypes();
                string innerComma = "";

                sb.Append(outerComma);
                sb.Append(target.Name);
                sb.Append('(');
                foreach (Type t in types) {
                    sb.Append(innerComma);
                    sb.Append(GetTypeName(t));
                    innerComma = ", ";
                }

                sb.Append(')');
                outerComma = ", ";
            }

            return ErrorInfo.FromException(
                Ast.Expression.Call(
                    typeof(BinderOps).GetMethod("SimpleTypeError"),
                    Ast.Expression.Constant(sb.ToString(), typeof(string))
                )
            );
        }

        private ErrorInfo MakeCallFailureError(BindingTarget target) {
            foreach (CallFailure cf in target.CallFailures) {
                switch (cf.Reason) {
                    case CallFailureReason.ConversionFailure:
                        foreach (ConversionResult cr in cf.ConversionResults) {
                            if (cr.Failed) {
                                return ErrorInfo.FromException(
                                    Ast.Expression.Call(
                                        typeof(BinderOps).GetMethod("SimpleTypeError"),
                                        Ast.Expression.Constant(String.Format("expected {0}, got {1}", GetTypeName(cr.To), GetTypeName(cr.From)))
                                    )
                                );
                            }
                        }
                        break;
                    case CallFailureReason.DuplicateKeyword:
                        return ErrorInfo.FromException(
                                Ast.Expression.Call(
                                    typeof(BinderOps).GetMethod("TypeErrorForDuplicateKeywordArgument"),
                                    Ast.Expression.Constant(target.Name, typeof(string)),
                                    Ast.Expression.Constant(SymbolTable.IdToString(cf.KeywordArguments[0]), typeof(string))    // TODO: Report all bad arguments?
                            )
                        );
                    case CallFailureReason.UnassignableKeyword:
                        return ErrorInfo.FromException(
                                Ast.Expression.Call(
                                    typeof(BinderOps).GetMethod("TypeErrorForExtraKeywordArgument"),
                                    Ast.Expression.Constant(target.Name, typeof(string)),
                                    Ast.Expression.Constant(SymbolTable.IdToString(cf.KeywordArguments[0]), typeof(string))    // TODO: Report all bad arguments?
                            )
                        );
                    default: throw new InvalidOperationException();
                }
            }
            throw new InvalidOperationException();
        }
        
        
        /// <summary>
        /// Checks to see if the language allows keyword arguments to be bound to instance fields or
        /// properties and turned into sets.  By default this is only allowed on contructors.
        /// </summary>
        protected internal virtual bool AllowKeywordArgumentSetting(MethodBase method) {
            return CompilerHelpers.IsConstructor(method);
        }
    }
}

